package br.com.linkin.mi.utils.data;


import br.com.linkin.mi.utils.validation.Validator;
import java.text.DateFormat;
import java.text.ParseException;
import java.text.SimpleDateFormat;

import java.util.Date;

/**
 *
 * @author Daniel Mendonca
 */

/**
 * Classe que representa e valida uma Data.
 */
 public final class Data implements Validator, Formatter {
 /** Número de dígitos da Data */
    public static final short LENGTH = 8;
    /** Máscara da Data */
    public static final String MASK = "dd/MM/yyyy";
    /** Formatador padrão de datas */
    private static final DateFormat dateFormat = new SimpleDateFormat(MASK);

    /** Armazena o valor original atribuído à Data */
    private String value = null;
    
    /**
     * Construtor padrão (default constructor).
     */
    public Data() {
        this((String)null);
    }

    /**
     * Construtor que recebe a Data como argumento.
     * <br>
     * Exemplos: <br>
     * <code>Data d1 = new Data("01012007"); // sem formatação <br>
     * Data d2 = new Data("01/01/2007"); // formatado <br>
     * Data d3 = new Data("01-01-2007"); // formatado</code>
     * <br> 
     * Qualquer outro formato passado como argumento pode causar uma Data inválida.
     * 
     * @param data Data.
     */
    public Data( String data ) {
        setValue(data);
    }

    /**
     * Construtor que recebe a Data como argumento.
     * <br>
     * Exemplo: <br>
     * <code>Data d = new Data(new java.util.Date());</code>
     * 
     * @param data <code>java.util.Date</code> que representa a Data.
     */
    public Data( Date data ) {
        setValue( dateFormat.format(data) );
    }
    
    /**
     * Atribui um novo valor à Data. Pode ser informada com ou sem 
     * formatação. Exemplos: "01012007" ou "01/01/2007" ou "01-01-2007".
     * 
     * @param data Data.
     */
    public void setValue(String data) {
        this.value = data;
    }
    
    /**
     * Retorna o valor original atribuido à Data.
     * 
     * @return Valor da Data.
     */
    public String getValue() {
        return this.value;
    }
    
    /**
     * Retorna a máscara de formatação utilizada.
     * 
     * @return Máscara de formatação.
     */
    public String getMask() {
        return MASK;
    }

    /**
     * Retorna a Data formatada. Retorna null se a Data não foi informada, ou 
     * retorna uma String vazia ("") se a Data informada não é válida.
     * 
     * @return Data formatada.
     */
    public String getFormatted() {
        return format(getValue());
    }
    
    /**
     * Formata a Data informada, usando a máscara dd/MM/yyyy.
     * 
     * @param data Data (ex: "01012007")
     * 
     * @return Data formatada. Retorna uma String vazia ("") se a Data não foi 
     * informada ou se está no formato inválido.
     */
    public static String format( String data ) {
        return isValidFormat(data) ?
               data.replaceAll("(\\d{2})(\\d{2})(\\d{4})", "$1/$2/$3") : "";
    }
    
    /**
     * Formata a Data informada, usando a máscara dd/MM/yyyy.
     * 
     * @param data <code>java.util.Date</code>
     * 
     * @return Data formatada. Retorna uma String vazia ("") se a Data não foi 
     * informada ou se é inválida.
     */
    public static String format( Date data ) {
        return dateFormat.format( data );
    }

    /**
     * Determina se o formato da Data é válido.
     * 
     * @return true se a Data tem formato válido ou false caso contrário.
     */
    public boolean isValidFormat() {
        return isValidFormat(getValue());
    }

    /**
     * Determina se o formato da Data é válido.
     * 
     * @param data Data formatada (ex: "01012007" ou "01/01/2007" ou "01-01-2007").
     * 
     * @return true se a Data tem formato válido ou false caso contrário.
     */
    public static boolean isValidFormat(String data) {
        if (data == null) {
            return false;
        }
        return data.matches("\\d{2}[/-]?\\d{2}[/-]?\\d{4}");
    }

    /**
     * Verifica se a Data é valida.
     * 
     * @return true se for uma Data válida ou false caso contrário.
     */
    public boolean isValid() {
        return isValid(getValue());
    }

    /**
     * Verifica se a Data informada é valido.
     * @param data - String
     * 
     * @return true se for uma Data válida ou false caso contrário.
     */
    public static boolean isValid(String data) {
        if (data == null) {
            return false;
        }
        if( !data.matches("\\d{2}[/-]?\\d{2}[/-]?\\d{4}") ) {
            return false;
        }
        data = cleanup(data);
        int day = Integer.parseInt(data.substring(0,2));
        int month = Integer.parseInt(data.substring(2,4));
        int year = Integer.parseInt(data.substring(4));
        if (day < 1 || day > 31) { // valida o dia
            return false;
        }
        if (month < 1 || month > 12) { // valida o mês
            return false;
        }
        if ((month == 4 || month == 6 || month == 9 || month == 11) && day == 31) {
            return false;
        }
        if (month == 2) { // valida dia 29 de fevereiro
            if (day > 29 || (day == 29 && !isLeapYear(year))) {
                return false;
            }
        }
        return true; // data válida
    }

    /**
     * Verifica se o ano da Data é bissexto.
     * 
     * @return true se for ano bissexto ou false caso contrário.
     */
    public boolean isLeapYear() {
        return isLeapYear(this.getYear());
    }

    /**
     * Retorna o ano da Data. Se for uma Data incompleta ou inválida retorna -1.
     * 
     * @return Ano da Data.
     */
    public int getYear() {
        try {
            return !isValid() ? -1 :
                Integer.parseInt(cleanup(getValue()).substring(4));
        } catch (Exception e) {
            return -1;
        }
    }

    /**
     * Retorna o mês da Data. Se for uma Data incompleta ou inválida retorna -1.
     * 
     * @return Mês da Data.
     */
    public int getMonth() {
        try {
            return !isValid() ? -1 :
                Integer.parseInt(cleanup(getValue()).substring(2,4));
        } catch (Exception e) {
            return -1;
        }
    }

    /**
     * Retorna o dia da Data. Se for uma Data incompleta ou inválida retorna -1.
     * 
     * @return Dia da Data.
     */
    public int getDay() {
        try {
            return !isValid() ? -1 :
                Integer.parseInt(cleanup(getValue()).substring(0,2));
        } catch (Exception e) {
            return -1;
        }
    }
    
    /**
     * Verifica se o ano informado é bissexto.
     * 
     * @param ano Ano.
     * 
     * @return true se for ano bissexto ou false caso contrário.
     */
    public static boolean isLeapYear(int ano) {
        return (ano % 4 == 0 && (ano % 100 != 0 || ano % 400 == 0));
    }
    
    /**
     * Retira os caracteres não numéricos.
     * 
     * @param data Data.
     * 
     * @return Retorna uma nova String apenas com caracteres numéricos (se houver).
     */
    private static String cleanup( String data ) {
        return data.replaceAll("\\D*", "");
    }

    /**
     * Verifica se a Data é igual ao objeto informado. Deve ser passado como
     * argumento um outro objeto Data, ou uma String de Data formatada 
     * (ex: "01/01/2007").
     */
    @Override
    public boolean equals(Object obj) {
        return isValidFormat() && this.toString().equals(obj.toString());
    }
    
    /**
     * Retorna o hashcode.
     * 
     * @return hashcode.
     */
    @Override
    public int hashCode() {
        return this.getValue().hashCode();
    }

    /**
     * Retorna a Data formatada.
     * 
     * @return Data formatada.
     */
    @Override
    public String toString() {
        return getFormatted();
    }
    
    /**
     * Retorna um objeto Data representando o valor passado como argumento.
     * 
     * @param data String com a Data (formatada ou não).
     * 
     * @return Novo objeto Data representando o valor passado.
     */
    public Data valueOf( String data ) {
        return new Data(data);
    }
    
    /**
     * Retorna um objeto Data representando o valor passado como argumento.
     * 
     * @param data Data.
     * 
     * @return Novo objeto Data representando o valor passado.
     */
    public Data valueOf( Date data ) {
        return new Data(data);
    }
    
    /**
     * Retorna um objeto do tipo java.util.Date representando a data deste
     * objeto. Caso o valor do objeto seja uma data inválida, será retornado 
     * null.
     * 
     * @return Date que representa a data válida do objeto.
     */
    public Date toDate() {
        if( isValid() ) {
            try {
                return dateFormat.parse(getFormatted());
            } catch( ParseException e ) {
                return null;
            }
        }
        return null;
    }
 }
